import { readFileSync } from 'node:fs'
import { ApiReference } from '@scalar/nextjs-api-reference'
import type { ApiReferenceConfiguration } from '@scalar/types/api-reference'
import { sync } from 'fast-glob'
import type { NextRequest } from 'next/server'
import type { OpenAPIV3_1 } from 'openapi-types'
import {
  type CompilerHost,
  JSDocParsingMode,
  ScriptKind,
  ScriptTarget,
  createProgram,
  createSourceFile,
} from 'typescript'

import { getPathSchema } from './path'

/**
 * Scalar OpenAPI config
 */
export type OpenAPIConfig = {
  /**
   * The base directory for your API files
   * @default 'app/api'
   */
  apiDirectory?: string
} & Partial<ApiReferenceConfiguration>

// TODO switch to watcher
// @see https://github.com/microsoft/TypeScript-wiki/blob/main/Using-the-Compiler-API.md#writing-an-incremental-program-watcher
const compilerHost: CompilerHost = {
  fileExists: () => true,
  getCanonicalFileName: (filename) => filename,
  getCurrentDirectory: () => '',
  getDefaultLibFileName: () => '',
  getNewLine: () => '\n',
  getSourceFile: (filename) =>
    createSourceFile(filename, readFileSync(filename).toString(), ScriptTarget.Latest, false, ScriptKind.TS),
  jsDocParsingMode: JSDocParsingMode.ParseAll,
  readFile: () => undefined,
  useCaseSensitiveFileNames: () => true,
  writeFile: () => null,
}

/** OpenAPI 3.1.0 Spec */
const spec: OpenAPIV3_1.Document = {
  openapi: '3.1.0',
  info: {
    title: `${process.env.npm_package_name} - Next.js OpenAPI Spec by Scalar`,
    description:
      process.env.npm_package_description ||
      'This file has been autogenerated, check out the docs to see how to customize these options',
    version: process.env.npm_package_version || '0.0.0',
  },
  paths: {},
}

/**
 * Scalar Next.js OpenAPI schema generation
 *
 * Handles both the schema generation as well as the references
 *
 * TODO:
 * - Docs
 * - file watcher
 * - caching
 */
export const OpenAPI = (config: OpenAPIConfig = {}) => {
  const apiDirectory = config.apiDirectory ?? 'app/api'
  /** All ts files required for the schema */
  const programFileNames = sync(apiDirectory + '/**/*.ts')
  /** Specific route files to generate paths */
  const routeFileNames = sync(apiDirectory + '/**/route.ts')

  const program = createProgram(
    programFileNames,
    {
      noResolve: true,
      target: ScriptTarget.Latest,
    },
    compilerHost,
  )

  // Loop over all non scalar files to extract openapi data using AST
  routeFileNames.forEach((fileName) => {
    const sourceFile = program.getSourceFile(fileName)

    if (sourceFile) {
      const resp = getPathSchema(sourceFile, program)

      // Grab the path from the fileName
      const rawPath = fileName
        .replace(/^(?:src\/)?app|\/route\.ts$/g, '')
        .replace(/\[/g, '{')
        .replace(/\/\(\w+\)/g, '')
        .replace(/]/g, '}')
      const path = rawPath.startsWith('/') ? rawPath : '/' + rawPath

      spec.paths[path] = resp
    }
  })

  return {
    GET: async (req: NextRequest) => {
      // OpenAPI Schema JSON
      if (req.nextUrl.pathname.endsWith('.json')) {
        // Just grabbing the origin here but we can be smarter and look for the greatest common few segments
        spec.servers = [{ url: req.nextUrl.origin }]

        return Response.json(spec)
      }
      // References

      return await ApiReference({
        url: req.nextUrl.pathname + '/schema.json',
      })()
    },
  }
}
